﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Mail;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;

namespace NHINDirect
{
    public class NHINDAgent
    {
        Cryptographer m_cryptographer;
        ICertificateService m_internalCertService;
        ICertificateStore m_externalCertStore;
        ITrustSettingsStore m_trustSettings;
        TrustModel m_trustModel;
        bool m_encryptionEnabled = true;
        TrustEnforcementStatus m_minTrustRequirement;
        string m_domain;

        public NHINDAgent(string domain)
            : this(domain, SystemX509Store.OpenPrivate().Index(), 
                           SystemX509Store.OpenExternal().Index(),
                           new UniformTrustSettings(SystemX509Store.OpenAnchor()))
        {
        }
        
        public NHINDAgent(string domain, X509Certificate2 privateCert, X509Certificate2 externalCert, X509Certificate2 trustAnchor)
            : this(domain, new UniformCertificateService(privateCert), new UniformCertificateStore(externalCert), new UniformTrustSettings(trustAnchor))
        {
        }
                
        public NHINDAgent(string domain, ICertificateService internalCerts, ICertificateStore externalCerts, ITrustSettingsStore trustSettings)
            : this(domain, internalCerts, externalCerts, trustSettings, TrustModel.Default, Cryptographer.Default)
        {
        }

        public NHINDAgent(string domain, ICertificateService internalCerts, ICertificateStore externalCerts, ITrustSettingsStore trustSettings, TrustModel trustModel, Cryptographer cryptographer)
        {
            if (string.IsNullOrEmpty(domain) || internalCerts == null || externalCerts == null || trustSettings == null || trustModel == null || cryptographer == null)
            {
                throw new ArgumentException();
            }

            this.m_domain = domain;
            this.m_internalCertService = internalCerts;
            this.m_externalCertStore = externalCerts;
            this.m_cryptographer = cryptographer;
            this.m_trustSettings = trustSettings;
            this.m_trustModel = trustModel;
            this.m_minTrustRequirement = TrustEnforcementStatus.Success_Offline;
        }

        public string Domain
        {
            get
            {
                return this.m_domain;
            }
        }

        public Cryptographer Cryptographer
        {
            get
            {
                return this.m_cryptographer;
            }
        }

        public bool EncryptMessages
        {
            get
            {
                return this.m_encryptionEnabled;
            }
            set
            {
                this.m_encryptionEnabled = value;
            }
        }

        public ICertificateStore ExternalCertStore
        {
            get
            {
                return this.m_externalCertStore;
            }
        }

        public ICertificateService InternalCertService
        {
            get
            {
                return this.m_internalCertService;
            }
        }

        public ITrustSettingsStore TrustSettings
        {
            get
            {
                return this.m_trustSettings;
            }
        }

        /// <summary>
        /// Messages must satisfy this minimum trust status
        /// </summary>
        public TrustEnforcementStatus MinTrustRequirement
        {
            get
            {
                return this.m_minTrustRequirement;
            }
            set
            {
                if (value < TrustEnforcementStatus.Success_Offline)
                {
                    throw new ArgumentException();
                }
                this.m_minTrustRequirement = value;
            }
        }

        //
        // You can participate in the agent pipeline by subscribing to these events
        // You can choose to do FURTHER post-processing on the message:
        //   - adding headers
        //   - throwing exceptions
        // If you throw an exception, message processing is ABORTED
        //
        public event Action<NHINDAgent, Exception> Error;
        public event Action<IncomingMessage> PreProcessIncoming;
        public event Action<IncomingMessage> PostProcessIncoming;
        public event Action<IncomingMessage, Exception> ErrorIncoming;
        public event Action<OutgoingMessage> PreProcessOutgoing;
        public event Action<OutgoingMessage> PostProcessOutgoing;
        public event Action<OutgoingMessage, Exception> ErrorOutgoing;

        //-------------------------------------------------------------------
        //
        // INCOMING MESSAGE
        //
        //-------------------------------------------------------------------

        /// <summary>
        /// Incoming message from an external sender to somebody in this org...
        ///  - Decrypt, Verify Signature, etc
        ///  Takes raw message text and returns raw message text
        /// </summary>
        public string ProcessIncoming(string messageText)
        {
            if (string.IsNullOrEmpty(messageText))
            {
                throw new ArgumentException();
            }

            IncomingMessage message = EntitySerializer.Default.Deserialize<IncomingMessage>(messageText);
            
            this.ProcessIncoming(message);
            
            return EntitySerializer.Default.Serialize(message);
        }

        /// <summary>
        /// Incoming message from an external sender to somebody in this org...
        ///  - Decrypt, Verify Signature, etc
        ///  
        /// Encryption and signature envelopes/artifacts are removed from the message and the original content 
        /// is restored. 
        /// </summary>
        public void ProcessIncoming(IncomingMessage message)
        {
            if (message == null)
            {
                throw new ArgumentException();
            }

            message.Agent = this;
            try
            {
                message.Validate();

                this.Notify(message, this.PreProcessIncoming);

                this.ProcessMessage(message);

                this.Notify(message, this.PostProcessIncoming);
                //
                // Some recipients may not trust this message. Remove them from the To list to prevent message delivery
                //
                message.UpdateTo();                
            }
            catch (Exception error)
            {
                this.Notify(message, error);
                throw;  // rethrow error
            }
        }

        void ProcessMessage(IncomingMessage message)
        {
            if (message.Sender == null)
            {
                throw new TrustException(TrustError.UntrustedSender);
            }

            message.CategorizeRecipients(this.Domain);
            if (!message.HasDomainRecipients)
            {
                throw new TrustException(TrustError.NoTrustedRecipients);
            }
            //
            // Map each address to its certificates/trust settings
            //
            this.BindAddresses(message);
            //
            // Extract signed content from the message
            //
            SignedEntity signedEntity = this.DecryptSignedContent(message);
            //
            // Extract the signature. 
            //
            message.Signature = this.m_cryptographer.DeserializeSignatureEnvelope(signedEntity);
            //
            // Alter body to contain actual content. Also clean up mime headers on the message that were there to support
            // signatures etc
            //
            message.Headers = message.Headers.SelectNonMimeHeaders();
            message.ApplyBody(signedEntity.Content); // this will merge in content + content specific mime headers
            //
            // Enforce trust requirements, including checking signatures
            //
            this.m_trustModel.Enforce(message);
            //
            // Remove any untrusted recipients...
            //
            if (message.HasDomainRecipients)
            {
                message.CategorizeRecipients(this.m_minTrustRequirement);
            }
            if (!message.HasDomainRecipients)
            {
                throw new TrustException(TrustError.NoTrustedRecipients);
            }
        }

        void BindAddresses(IncomingMessage message)
        {
            //
            // Retrieving the sender's certificate is optional
            //
            message.Sender.Certificate = this.ResolvePublicCert(message.Sender, false);
            //
            // Bind each recpient's certs and trust settings
            //
            NHINDAddressCollection recipients = message.DomainRecipients;
            for (int i = 0, count = recipients.Count; i < count; ++i)
            {
                NHINDAddress recipient = recipients[i];
                recipient.Certificate = this.ResolvePrivateCert(recipient, false);
                recipient.TrustAnchors = this.m_trustSettings.GetTrustAnchorsIncoming(recipient);
            }
        }

        SignedEntity DecryptSignedContent(IncomingMessage message)
        {
            SignedEntity signedEntity;
            if (this.m_encryptionEnabled)
            {
                //            
                // Decrypt and parse message body into a signature entity - the envelope that contains our data + signature
                // We can use the cert of any ONE of the recipients to decrypt
                //
                X509Certificate2 decryptingCert = message.DomainRecipients.GetFirstCertificate();
                if (decryptingCert == null)
                {
                    throw new TrustException(TrustError.MissingRecipientCertificate);
                }
                MimeEntity decryptedEntity = this.m_cryptographer.Decrypt(message, decryptingCert);
                //
                // Extract the signature envelope. That contains both the signature and the actual message content
                //
                signedEntity = SignedEntity.Load(decryptedEntity);
            }
            else
            {
                signedEntity = SignedEntity.Load(message);
            }

            return signedEntity;
        }

        //-------------------------------------------------------------------
        //
        // OUTGOING MESSAGE
        //
        //-------------------------------------------------------------------

        /// <summary>
        /// Outgoing message from somebody in this org to the outside world
        ///  - Sign, Encrypt
        ///  Takes raw message text and returns encrypted raw message text
        /// </summary>
        public string ProcessOutgoing(string messageText)
        {
            if (string.IsNullOrEmpty(messageText))
            {
                throw new ArgumentException();
            }

            OutgoingMessage message = EntitySerializer.Default.Deserialize<OutgoingMessage>(messageText);
            
            this.ProcessOutgoing(message);
            
            return EntitySerializer.Default.Serialize(message);
        }

        /// <summary>
        /// Outgoing message from somebody in this org to the outside world
        ///  - Sign, Encrypt
        ///  
        /// The message body is updated with signed+encrypted content. Headers are appropriately set
        /// </summary>
        public void ProcessOutgoing(OutgoingMessage message)
        {
            if (message == null)
            {
                throw new ArgumentException();
            }

            message.Agent = this;            
            try
            {
                message.Validate();

                this.Notify(message, this.PreProcessOutgoing);

                this.ProcessMessage(message);

                this.Notify(message, this.PostProcessOutgoing);
                //
                // Not all recipients may be trusted. Remove them from the To list to prevent delivery
                //
                message.UpdateTo();
            }
            catch (Exception error)
            {
                this.Notify(message, error);
                throw;
            }
        }

        void ProcessMessage(OutgoingMessage message)
        {
            if (message.Sender == null)
            {
                throw new ProtocolException(ProtocolError.MissingFrom);
            }

            this.BindAddresses(message);
            if (!message.HasRecipients)
            {
                throw new ProtocolException(ProtocolError.MissingTo);
            }
            //
            // Enforce the trust model.
            //
            this.m_trustModel.Enforce(message);
            //
            // Remove any non-trusted recipients
            //
            message.CategorizeRecipients(this.m_minTrustRequirement);
            if (!message.HasRecipients)
            {
                throw new TrustException(TrustError.NoTrustedRecipients);
            }
            //
            // Finally, sign and encrypt the message
            //
            this.SignAndEncryptMessage(message);
        }

        void BindAddresses(OutgoingMessage message)
        {
            //
            // Retrieving the sender's private certificate is requied for encryption
            //
            message.Sender.Certificate = this.ResolvePrivateCert(message.Sender, true);
            message.Sender.TrustAnchors = this.m_trustSettings.GetTrustAnchorsOutgoing(message.Sender);
            //
            // Bind each recipient's certs
            //
            NHINDAddressCollection recipients = message.Recipients;
            for (int i = 0, count = recipients.Count; i < count; ++i)
            {
                NHINDAddress recipient = recipients[i];
                recipient.Certificate = this.ResolvePublicCert(recipient, false);
            }
        }

        //
        // First sign, THEN encrypt the message
        //
        void SignAndEncryptMessage(OutgoingMessage message)
        {
            SignedEntity signedEntity = this.m_cryptographer.Sign(message, message.Sender.Certificate);

            if (this.m_encryptionEnabled)
            {
                MimeEntity encryptedEntity = this.m_cryptographer.Encrypt(signedEntity, message.Recipients.GetCertificates());
                //
                // Alter message content to contain encrypted data
                //
                message.ApplyBody(encryptedEntity);
            }
            else
            {
                message.ApplyBody(signedEntity);
            }
        }

        MailAddress ResolveFrom(Message message)
        {
            MailAddress from = message.ParsedFrom;
            if (from == null)
            {
                throw new ProtocolException(ProtocolError.MissingFrom);
            }

            return from;
        }

        X509Certificate2 ResolvePrivateCert(MailAddress address, bool required)
        {
            X509Certificate2 cert = null;
            try
            {
                cert = this.m_internalCertService.GetPrivateCertificate(address);
                if (cert == null && required)
                {
                    throw new TrustException(TrustError.UnknownRecipient);
                }
            }
            catch (Exception ex)
            {
                if (required)
                {
                    throw;
                }
                this.Notify(ex); // for logging, tracking etc...
            }

            return cert;
        }

        X509Certificate2 ResolvePublicCert(MailAddress address, bool required)
        {
            X509Certificate2 cert = null;
            try
            {
                if (address.DomainEquals(this.m_domain))
                {
                    //
                    // Internal address
                    //
                    cert = this.m_internalCertService.GetCertificate(address);
                }
                else
                {
                    cert = this.m_externalCertStore.GetCertificate(address);
                }

                if (cert == null && required)
                {
                    throw new TrustException(TrustError.UnknownRecipient);
                }
            }
            catch (Exception ex)
            {
                if (required)
                {
                    throw;
                }
                this.Notify(ex); // for logging, tracking etc...
            }

            return cert;
        }

        void Notify(IncomingMessage message, Action<IncomingMessage> eventHandler)
        {
            //
            // exceptions are interpreted as: abort message
            //
            if (eventHandler != null)
            {
                eventHandler(message);
            }
        }

        void Notify(OutgoingMessage message, Action<OutgoingMessage> eventHandler)
        {
            //
            // exceptions are interpreted as: abort message
            //
            if (eventHandler != null)
            {
                eventHandler(message);
            }
        }

        void Notify(IncomingMessage message, Exception ex)
        {
            try
            {
                if (this.ErrorIncoming != null)
                {
                    this.ErrorIncoming(message, ex);
                }
            }
            catch
            {
            }
        }

        void Notify(OutgoingMessage message, Exception ex)
        {
            try
            {
                if (this.ErrorOutgoing != null)
                {
                    this.ErrorOutgoing(message, ex);
                }
            }
            catch
            {
            }
        }

        void Notify(Exception ex)
        {
            try
            {
                if (this.Error != null)
                {
                    this.Error(this, ex);
                }
            }
            catch
            {
            }
        }
    }
}
